﻿using System;
using System.IO;
using Pfz.Extensions;
using Pfz.Threading;

namespace Pfz.Remoting.Udp
{
	/// <summary>
	/// TcpIp connection that has an UDP connection bound to it, so you can opt to send guaranteed and ordered packets or
	/// udp (optional) packets. Also allows you to configure it so it can use a guaranteed udp version, in which case
	/// packets are unordered, but are guaranteed to arrive.
	/// </summary>
	public sealed class TcpWithUdpConnection:
		ThreadSafeDisposable,
		IConnection,
		IGuaranteedUdpConnection
	{
		private TcpWithUdpListener _listener;
		private DisposableTcpConnection _connection;
		private GuaranteedUdpConnection _udp;

		/// <summary>
		/// Creates and configures a new TcpWithUdpConnection instance.
		/// </summary>
		public TcpWithUdpConnection(string hostname, int port, bool useTcpDelay=true, int bufferedWriteStreamSize=-1, int pendingGuaranteedDeliveryCount=57)
		{
			try
			{
				_udp = new GuaranteedUdpConnection(hostname, port+1, pendingGuaranteedDeliveryCount);

				var startedEvent = new ManagedManualResetEvent();
				byte[] bytes = null;

				UnlimitedThreadPool.Run
				(
					() =>
					{
						try
						{
							bytes = _udp.UdpReceive();
						}
						catch
						{
							Dispose();
						}
						finally
						{
							startedEvent.Set();
						}
					}
				);

				if (!startedEvent.WaitOne(60000))
					throw new TimeoutException("UDP handshaking timed-out.");

				if (bytes == null)
					throw new IOException("Can't stablish UDP connection.");

				if (bytes.Length != _udp.UdpHeaderSize + 16)
					throw new IOException("The other side sent an invalid handshaking packet.");

				_connection = new DisposableTcpConnection(hostname, port, useTcpDelay, bufferedWriteStreamSize);
				var stream = _connection.Stream;
				stream.Write(bytes, _udp.UdpHeaderSize);
				stream.Flush();
			}
			catch
			{
				Dispose();
				throw;
			}
		}
		internal TcpWithUdpConnection(TcpWithUdpListener listener, DisposableTcpConnection connection, GuaranteedUdpConnection udp)
		{
			_listener = listener;
			_connection = connection;
			_udp = udp;
		}

		/// <summary>
		/// Releases the tcp and udp connections.
		/// </summary>
		protected override void Dispose(bool disposing)
		{
			if (disposing)
			{
				var listener = _listener;
				if (listener != null)
				{
					_listener = null;

					lock(listener.DisposeLock)
						if (!listener.WasDisposed)
							listener._activeConnections.Remove(this);
				}

				Disposer.Dispose(ref _connection);
				Disposer.Dispose(ref _udp);
			}

			base.Dispose(disposing);
		}

		/// <summary>
		/// Gets the local endpoint.
		/// </summary>
		public string LocalEndpoint
		{
			get
			{
				var connection = _connection;
				if (connection == null)
					return null;

				return connection.LocalEndpoint;
			}
		}

		/// <summary>
		/// Gets the remote endpoint.
		/// </summary>
		public string RemoteEndpoint
		{
			get
			{
				var connection = _connection;
				if (connection == null)
					return null;

				return connection.RemoteEndpoint;
			}
		}

		/// <summary>
		/// Gets or sets the stream used for the ordered (tcp/ip) communication.
		/// </summary>
		public Stream Stream
		{
			get
			{
				var connection = _connection;
				if (connection == null)
					return null;

				return connection.Stream;
			}
			set
			{
				lock(DisposeLock)
				{
					CheckUndisposed();

					_connection.Stream = value;
				}
			}
		}

		/// <summary>
		/// Sends an UDP packet. This packet can be lost.
		/// </summary>
		public void UdpSend(byte[] buffer, int offset, int count)
		{
			lock(DisposeLock)
			{
				CheckUndisposed();

				_udp.UdpSend(buffer, offset, count);
			}
		}

		/// <summary>
		/// Receives an UDP packet. Note that it may include a header, so check the UdpHeaderSize property.
		/// </summary>
		public byte[] UdpReceive()
		{
			var udp = _udp;
			if (udp == null)
				return null;

			return udp.UdpReceive();
		}

		/// <summary>
		/// Gets the size of the header added to all packets.
		/// </summary>
		public int UdpHeaderSize
		{
			get
			{
				var udp = _udp;
				if (udp == null)
					return 0;

				return udp.UdpHeaderSize;
			}
		}

		/// <summary>
		/// Sends a message with guarantee of delivery. It does not guarantees the order, however.
		/// </summary>
		public void GuaranteedSend(byte[] buffer, int offset, int count)
		{
			lock(DisposeLock)
			{
				CheckUndisposed();

				_udp.GuaranteedSend(buffer, offset, count);
			}
		}

		/// <summary>
		/// Checks if a packet was sent as guaranteed (true) or not.
		/// </summary>
		public bool IsGuaranteedPacket(byte[] packet)
		{
			lock(DisposeLock)
			{
				CheckUndisposed();

				return _udp.IsGuaranteedPacket(packet);
			}
		}
	}
}
